package com.errol.generic.file.excel.pojo;

import com.renjia.service.generic.base.util.DateFormatUtil;

import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import javax.validation.constraints.NotNull;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.util.Date;
import java.util.Map;
import java.util.Set;

/**
 * Created by Errol1017 on 2018/6/16.
 *
 * @author zhang xiao bin
 * @version v1.0
 * @date 2020/10/21
 * @since v4.7.6
 */
public class ExcelColumnDescriptor {

    private static final String formatErrMsg = " | 数据格式错误";
    private static final String dynamicErrMsg = " | 动态校验错误";

    private final ExcelColConfig excelColConfig;
    private PropertyDescriptor propertyDescriptor;
    private Field field;
    private ExcelColumnDescriptor child;
    private ValueType valueType;

    public <R> ExcelColumnDescriptor(Field field, Class<R> clazz, ExcelColumnDescriptor parentDescriptor) {
        try {
            if (parentDescriptor == null) {
                this.field = field;
                this.excelColConfig = field.getAnnotation(ExcelColConfig.class);
                this.propertyDescriptor = new PropertyDescriptor(field.getName(), clazz);
            } else {
                this.field = parentDescriptor.field;
                this.excelColConfig = parentDescriptor.excelColConfig;
                this.propertyDescriptor = parentDescriptor.propertyDescriptor;
                if (parentDescriptor.child != null) {
                    this.child = new ExcelColumnDescriptor(field, clazz, parentDescriptor.child);
                } else {
                    this.child = new ExcelColumnDescriptor(field, clazz, null);
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public <R> ExcelColumnDescriptor(ExcelColConfig excelColConfig, ValueType valueType, ExcelColumnDescriptor parentDescriptor) {
        if (parentDescriptor == null) {
            this.excelColConfig = excelColConfig;
            this.valueType = valueType;
        } else {
            this.excelColConfig = parentDescriptor.excelColConfig;
            if (parentDescriptor.child != null) {
                this.child = new ExcelColumnDescriptor(excelColConfig, valueType, parentDescriptor.child);
            } else {
                this.child = new ExcelColumnDescriptor(excelColConfig, valueType, null);
            }
        }
    }

    public ExcelColumnDescriptor getChildAt(int level) {
        ExcelColumnDescriptor child = this;
        int l = 0;
        while (l < level && child != null) {
            child = child.child;
            l++;
        }
        return child;
    }

    public ExcelColumnDescriptor getChildDeepest() {
        ExcelColumnDescriptor child = this;
        while (child.child != null) {
            child = child.child;
        }
        return child;
    }

    public ValueType calValueType() throws NoSuchMethodException {
        if (valueType != null) {
            return valueType;
        }
        ExcelColumnDescriptor child = getChildDeepest();
        String typeString = child.propertyDescriptor.getPropertyType().toString();
        if (!typeString.startsWith("class")) {
            throw new RuntimeException("type of field comment by @ExcelColConfig should been packaging class");
        }
        if ("class java.lang.String".equals(typeString)) {
            valueType = ValueType.STRING;
        } else if ("class java.lang.Integer".equals(typeString)) {
            valueType = ValueType.INTEGER;
        } else if ("class java.lang.Long".equals(typeString)) {
            valueType = ValueType.LONG;
        } else if ("class java.lang.Boolean".equals(typeString)) {
            valueType = ValueType.BOOLEAN;
        } else if ("class java.lang.Double".equals(typeString)) {
            valueType = ValueType.DOUBLE;
        } else if ("class java.lang.Float".equals(typeString)) {
            valueType = ValueType.FLOAT;
        } else if ("class java.util.Date".equals(typeString)) {
            valueType = ValueType.DATE;
            if (child.excelColConfig.datePattern().equals("")) {
                throw new RuntimeException("field of Date type shall specify the date pattern in @ExcelColConfig");
            }
        } else if (child.propertyDescriptor.getPropertyType().isEnum()) {
            valueType = ValueType.ENUM;
            if (excelColConfig.dynamic().length() == 0) {
                throw new RuntimeException("field of Enum type shall use dynamic data source");
            }
        } else {
            throw new RuntimeException("unsupported field type: " + typeString);
        }
        child.valueType = valueType;
        return valueType;
    }

    public <R> Object invokeGetter(R obj) throws Exception {
        Object data = propertyDescriptor.getReadMethod().invoke(obj);
        if (child == null) {
            switch (calValueType()) {
                case BOOLEAN:
                    return (Boolean) data ? "是" : "否";
                case DATE:
                    return DateFormatUtil.date2String((Date) data, excelColConfig.datePattern());
                case ENUM:
                    return propertyDescriptor.getPropertyType().getMethod("name").invoke(data);
                default:
            }
            return data;
        } else {
            return child.invokeGetter(data);
        }
    }

    public <R> String invokeSetter(R obj, String v, Validator validator, Map<String, Map<String, String>> dynamicSources, boolean trim) throws Exception {
        if (child == null) {
            if (v != null) {
                if (trim) {
                    v = v.replaceAll("(^((\\s|\t|\r|\n)+))|(((\\s|\t|\r|\n)+)$)", "");
                    if (excelColConfig.compact()) {
                        v = v.replace(" ", "");
                    } else {
                        v = trim(v);
                    }
                }
                if (excelColConfig.dynamic().length() > 0) {
                    if (!dynamicSources.containsKey(excelColConfig.dynamic())) {
                        return dynamicErrMsg;
                    }
                    Map<String, String> dynamicSource = dynamicSources.get(excelColConfig.dynamic());
                    if (!dynamicSource.containsKey(v)) {
                        return dynamicErrMsg;
                    }
                    v = dynamicSource.get(v);
                }
                try {
                    switch (calValueType()) {
                        case STRING:
                            propertyDescriptor.getWriteMethod().invoke(obj, v);
                            break;
                        case INTEGER:
                            if (v.matches("^-?\\d{1,10}(\\.0)?0*$")) {
                                propertyDescriptor.getWriteMethod().invoke(obj, Double.valueOf(v).intValue());
                                break;
                            }
                            return formatErrMsg;
                        case LONG:
                            if (v.matches("^-?\\d{1,18}(\\.0)?0*$")) {
                                propertyDescriptor.getWriteMethod().invoke(obj, Double.valueOf(v).longValue());
                                break;
                            }
                            return formatErrMsg;
                        case BOOLEAN:
                            if (v.equals("是") || v.equals("否")) {
                                propertyDescriptor.getWriteMethod().invoke(obj, v.equals("是"));
                                break;
                            }
                            return formatErrMsg;
                        case DOUBLE:
                            if (v.matches("^-?\\d{1,18}(\\.\\d)?\\d*$")) {
                                propertyDescriptor.getWriteMethod().invoke(obj, Double.valueOf(v));
                                break;
                            }
                            return formatErrMsg;
                        case FLOAT:
                            if (v.matches("^-?\\d{1,10}(\\.\\d)?\\d*$")) {
                                propertyDescriptor.getWriteMethod().invoke(obj, Float.valueOf(v));
                                break;
                            }
                            return formatErrMsg;
                        case DATE:
                            if (!v.matches(excelColConfig.datePattern().replaceAll("\\w", "\\\\d"))) {
                                return formatErrMsg;
                            }
                            Date date = DateFormatUtil.string2Date(v, excelColConfig.datePattern());
                            if (date == null) {
                                return formatErrMsg;
                            }
                            propertyDescriptor.getWriteMethod().invoke(obj, date);
                            break;
                        case ENUM:
                            // 读取 excel 为 enum，先用 dynamic 将中文转为 枚举值 name，然后用 valueOf 转为枚举值
                            Object e = propertyDescriptor.getPropertyType().getMethod("valueOf", String.class).invoke(null, v);
                            propertyDescriptor.getWriteMethod().invoke(obj, e);
                            break;
                        default:
                            return formatErrMsg;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    return formatErrMsg;
                }
            }

            Set<ConstraintViolation<R>> violationSet = validator.validateProperty(obj, propertyDescriptor.getName());
            if (violationSet.size() > 0) {
                return " | " + violationSet.iterator().next().getMessage();
            }
            return null;
        } else {
            Object invoke = propertyDescriptor.getReadMethod().invoke(obj);
            if (v == null && invoke == null && !field.isAnnotationPresent(NotNull.class)) {
                return null;
            }
            if (invoke == null) {
                propertyDescriptor.getWriteMethod().invoke(obj, propertyDescriptor.getPropertyType().isMemberClass()
                        ? propertyDescriptor.getPropertyType().getDeclaredConstructor(obj.getClass()).newInstance(obj)
                        : propertyDescriptor.getPropertyType().getConstructor().newInstance());
                invoke = propertyDescriptor.getReadMethod().invoke(obj);
            }
            return child.invokeSetter(invoke, v, validator, dynamicSources, trim);
        }
    }

    public String trim(String v) {
        int s = -1;
        int e = -1;
        char[] chars = v.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            if (' ' != chars[i]) {
                s = i;
                break;
            }
        }
        if (s == -1) {
            return "";
        }
        for (int i = chars.length - 1; i >= 0; i--) {
            if (' ' != chars[i]) {
                e = i + 1;
                break;
            }
        }
        if (s == 0 && e == v.length()) {
            return v;
        }
        return v.substring(s, e);
    }

    public ExcelColConfig getExcelColConfig() {
        return excelColConfig;
    }

    public PropertyDescriptor getPropertyDescriptor() {
        return propertyDescriptor;
    }

    public ExcelColumnDescriptor getChild() {
        return child;
    }

    public ValueType getValueType() {
        return valueType;
    }
}
